/*
 * Justin Prentice
 * TCSS 305 (winter quarter)
 * Tetris program - Tetris Board
 */

package board;

import java.awt.Color;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Observable;
import java.util.Queue;

import piece.AbstractPiece;
import piece.IPiece;
import piece.JPiece;
import piece.LPiece;
import piece.OPiece;
import piece.SPiece;
import piece.TPiece;
import piece.ZPiece;

/**
 * A class representing a tetris board.
 * @author Justin Prentice
 * @version 1.0
 */
public class TetrisBoard extends Observable
{
  
  /**
   * The offset to put pieces in which aren't yet on the board.  
   */
  public static final int BOARD_OFFSET = 4;
  /**
   * The default board height.
   */
  private static final int BOARD_HEIGHT = 20;
  /**
   * The default board width.
   */
  private static final int BOARD_WIDTH = 10;

  /**
   * A queue to hold upcoming pieces.
   */
  private final Queue<AbstractPiece> my_upcoming_pieces;
  
  /**
   * An array of colors representing pieces that have been dropped at the bottom of the board.
   */
  private boolean[][] my_frozen_pieces;
  
  /**
   * The piece currently progressing toward the bottom.
   */
  private AbstractPiece my_current_piece;
  
  /**
   * A list of pieces for picking a random new piece.  
   */
  private List<AbstractPiece> my_piece_pool;
  
  /**
   * Constructs a tetris board of standard size (10 by 20).
   * @throws CloneNotSupportedException throws exception if pieces aren't cloneable
   */
  public TetrisBoard() throws CloneNotSupportedException
  {
    super();
    my_frozen_pieces = new boolean[BOARD_WIDTH][BOARD_HEIGHT + BOARD_OFFSET];
    my_upcoming_pieces = new LinkedList<AbstractPiece>();
    my_piece_pool = new ArrayList<AbstractPiece>();
    populateRandomPieces();
    randomPiece();
    my_current_piece = my_upcoming_pieces.poll();
    randomPiece(); //ensures that there is a piece in queue
  }
  /**
   * Constructs a tetris board of specified height and width.
   * @param the_width the custom width
   * @param the_height the custom height
   * @throws CloneNotSupportedException throws exception if pieces aren't cloneable
   */
  public TetrisBoard(final int the_width, 
                     final int the_height) throws CloneNotSupportedException
  {
    super();
    my_frozen_pieces = new boolean[the_width][the_height + BOARD_OFFSET];
    my_upcoming_pieces = new LinkedList<AbstractPiece>();
    my_piece_pool = new ArrayList<AbstractPiece>();
    populateRandomPieces();
    randomPiece();
    my_current_piece = my_upcoming_pieces.poll();
    randomPiece();
  }
  /**
   * Constructs a default size tetris board with a preset order of pieces to start with.
   * @param the_pieces An array of pieces to be inflicted on the user in a specific order.
   * @throws CloneNotSupportedException throws exception if the pieces aren't cloneable
   */
  public TetrisBoard(final AbstractPiece[] the_pieces) throws CloneNotSupportedException
  {
    super();
    my_frozen_pieces = new boolean[BOARD_WIDTH][BOARD_HEIGHT + BOARD_OFFSET];
    my_upcoming_pieces = new LinkedList<AbstractPiece>();
    for (int i = 0; i < the_pieces.length; i++)
    {
      my_upcoming_pieces.add(the_pieces[i]);
    }    
    my_piece_pool = new ArrayList<AbstractPiece>();
    populateRandomPieces();
    randomPiece();
    my_current_piece = my_upcoming_pieces.poll();
    randomPiece();
  }
  
  /**
   * Constructs a custom size tetris board with a preset order of pieces.
   * @param the_pieces an array of pieces to be inflicted on the user in the order provided
   * @param the_width the board width
   * @param the_height the board hight
   */
  public TetrisBoard(final AbstractPiece[] the_pieces, 
                     final int the_width, final int the_height)
  {
    super();
    my_frozen_pieces = new boolean[the_width][the_height + BOARD_OFFSET];
    my_upcoming_pieces = new LinkedList<AbstractPiece>();
    for (int i = 0; i < the_pieces.length; i++)
    {
      my_upcoming_pieces.add(the_pieces[i]);
    }    
    my_piece_pool = new ArrayList<AbstractPiece>();
    populateRandomPieces();
    my_current_piece = my_upcoming_pieces.poll();
  }
  
  /**
   * Helper method to fill the piece pool with one piece of each type.  
   */
  private void populateRandomPieces()
  {
    
    my_piece_pool.add(new IPiece());
    my_piece_pool.add(new JPiece());
    my_piece_pool.add(new LPiece());
    my_piece_pool.add(new OPiece());
    my_piece_pool.add(new SPiece());
    my_piece_pool.add(new TPiece());
    my_piece_pool.add(new ZPiece());
    for (AbstractPiece p : my_piece_pool)
    {
      p.setX(my_frozen_pieces.length / 2);
    }
  }
  
  /**
   * Adds a random piece to the piece queue. 
   * @throws CloneNotSupportedException Should not occur, as all pieces implement clone.
   */
  private void randomPiece() throws CloneNotSupportedException 
  {
    my_upcoming_pieces.add(my_piece_pool.get(
                                   (int) (Math.random() * my_piece_pool.size())).clone());
    //grabs a random index, clones it, adds it to piece queue
  }

  /**
   * Updates the tetris board one movement.
   * @throws CloneNotSupportedException Throws any exceptions from randomPiece().
   */
  public void tick() throws CloneNotSupportedException
  {    
    if (my_upcoming_pieces.isEmpty())
    {
      randomPiece();
    }
    down();
  }
  
  /**
   * Moves the current tetris piece down if possible.  
   * Freezes the piece otherwise. 
   * @throws CloneNotSupportedException throws exception if the piece isn't cloneable.
   */
  public void down() throws CloneNotSupportedException 
  {
    final AbstractPiece testpiece = my_current_piece.clone();
    testpiece.down();
    
    if (canMove(testpiece))
    {
      my_current_piece.down();
    }
    else //freeze the piece
    {
      freezePiece();
      my_current_piece = my_upcoming_pieces.poll();
      randomPiece();
      checkLines();
    }
    setChanged(); //a change will occur, either a freeze or down movement
    notifyObservers();
  }
  
  /**
   * Helper method to put frozen pieces into the game board.  
   */
  private void freezePiece()
  {
    final boolean[][] state = my_current_piece.getState();
    for (int i = 0; i < state.length; i++)
    {
      for (int j = 0; j < state[0].length; j++)
      {
        if (state[i][j])
        {
          my_frozen_pieces[my_current_piece.getX() + i]
                          [my_current_piece.getY() + j + BOARD_OFFSET] = true;
        }
      }
    }
  }
  
  /**
   * Moves the current piece to the left if possible.
   * @throws CloneNotSupportedException throws exception if the piece isn't cloneable.
   */
  public void left() throws CloneNotSupportedException
  {
    final AbstractPiece testpiece = my_current_piece.clone();
    testpiece.left();
    
    if (canMove(testpiece))
    {
      my_current_piece.left();
      setChanged();
      notifyObservers();
    }
    
  }
  
  /**
   * Moves the current piece to the right if possible.
   * @throws CloneNotSupportedException throws exception if the piece isn't cloneable.
   */
  public void right() throws CloneNotSupportedException
  {
    final AbstractPiece testpiece = my_current_piece.clone();
    testpiece.right();
    if (canMove(testpiece))
    {
      my_current_piece.right();
      setChanged();
      notifyObservers();
    }
  }
  
  /**
   * Rotates the piece, if possible.
   * @throws CloneNotSupportedException trhows exception if the piece is not cloneable
   */
  public void rotate() throws CloneNotSupportedException
  {
    final AbstractPiece testpiece = my_current_piece.clone();
    testpiece.rotate();
    if (canMove(testpiece))
    {
      my_current_piece.rotate();
      setChanged();
      notifyObservers();
    }
  }
  
  /**
   * Helper method to determine if the move is valid.
   * @param the_piece the transformed piece to be tested for validity.
   * @return Whether the move is legal (true) or not (false)
   */
  private boolean canMove(final AbstractPiece the_piece)
  {
    boolean canmove = true;
    final boolean[][] teststate = the_piece.getState();
    final int x = the_piece.getX(); //to avoid repeated function calls
    final int y = the_piece.getY();
    
    for (int i = 0; i < teststate.length; i++)
    {
      for (int j = 0; j < teststate[0].length; j++)
      {
        if (teststate[i][j]) //tests only if there is a block there
        {
          if (x + i >= my_frozen_pieces.length || //bounds checking
              x + i < 0 || 
              y + j >= my_frozen_pieces[0].length || 
              y < 0)
          {
            canmove = false;
          }
          else if (my_frozen_pieces[x + i][y + j]) //collision checking
          {
            canmove = false;
          }          
        }
      }
    }
    return canmove;
  }
  
  /**
   * Checks to see if any rows are full, and need be removed.  
   */
  private void checkLines()
  {
    for (int y = BOARD_OFFSET + my_frozen_pieces[0].length; y >= BOARD_OFFSET; y--)
    {
      boolean filled = true; //flag value, 
      for (int x = 0; x < my_frozen_pieces.length; x++) //check for full row
      {
        if (!my_frozen_pieces[x][y])
        {
          filled = false;
        }
      }
      if (filled) //a full row
      {
        deleteLines(y);
        setChanged();
        notifyObservers();
        y++;
      }
    }
  }
  
  /**
   * A helper method that removes a line from the array.
   * @param the_row the row to be deleted
   */
  private void deleteLines(final int the_row)
  {
    for (int y = the_row; y > 0; y--) //sets each piece equal to the piece above it.
    {
      for (int x = 0; x < my_frozen_pieces.length; x++)
      {
        my_frozen_pieces[x][y] = my_frozen_pieces[x][y - 1];
      }
    }
    for (int x = 0; x < my_frozen_pieces.length; x++) //fills the top row with false
    {
      my_frozen_pieces[x][0] = false;
    }
  }
  
  /**
   * Returns an array of booleans representing the frozen pieces.  
   * @return the array of booleans. 
   */
  public boolean[][] getState()
  {
    return my_frozen_pieces.clone();
  }
  
  /**
   * Returns the piece currently in motion.  
   * @return the current piece
   * @throws CloneNotSupportedException if the piece is not cloneable
   */
  public AbstractPiece getCurrentPiece() throws CloneNotSupportedException
  {
    return my_current_piece.clone();
  }
  
  /**
   * {@inheritDoc}
   */
  public String toString()
  {
    final StringBuilder sb = new StringBuilder();
    final boolean[][] board = my_frozen_pieces.clone();
    final boolean[][] state = my_current_piece.getState();
    
    for (int i = 0; i < state.length; i++) //freezes the current piece into the new array
    {
      for (int j = 0; j < state[0].length; j++)
      {
        if (state[i][j])
        {
          board[my_current_piece.getX() + i]
                          [my_current_piece.getY() + j + BOARD_OFFSET] = true;
        }
      }
    }
    
    for (int x = 0; x < my_frozen_pieces.length; x++) //prints the board
    {
      for (int y = BOARD_OFFSET - 1; y < my_frozen_pieces[0].length; y++)
      {
        if (my_frozen_pieces[x][y])
        {
          sb.append('x');
        }
        else
        {
          sb.append(' ');
        }
      }
      sb.append('\n');
    }
    return sb.toString();
  }
  
  /**
   * Allows the user to peek at the upcoming piece. 
   * @return the upcoming piece
   */
  public AbstractPiece upcomingPiece()
  {
    return my_upcoming_pieces.peek();
  }
}
